home *** CD-ROM | disk | FTP | other *** search
/ Enter 2006 September / Enter 09 2006.iso / Internet / SpamExperts Home 1.1 / SpamExperts Home.exe / lib / spamexperts.modules / spf.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2006-07-14  |  18.2 KB  |  688 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.4)
  3.  
  4. '''SPF (Sender-Permitted From) implementation.
  5.  
  6. Copyright (c) 2003, Terence Way
  7. This module is free software, and you may redistribute it and/or modify
  8. it under the same terms as Python itself, so long as this copyright message
  9. and disclaimer are retained in their original form.
  10.  
  11. IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
  12. SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
  13. THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
  14. DAMAGE.
  15.  
  16. THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
  17. LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  18. PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
  19. AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
  20. SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
  21.  
  22. For more information about SPF, a tool against email forgery, see
  23. \thttp://spf.pobox.com
  24.  
  25. For news, bugfixes, etc. visit the home page for this implementation at
  26. \thttp://www.wayforward.net/spf/
  27. '''
  28. __author__ = 'Terence Way'
  29. __email__ = 'terry@wayforward.net'
  30. __version__ = '1.6: December 18, 2003'
  31. MODULE = 'spf'
  32. USAGE = 'To check an incoming mail request:\n    % python spf.py {ip} {sender} {helo}\n    % python spf.py 69.55.226.139 tway@optsw.com mx1.wayforward.net\n\nTo test an SPF record:\n    % python spf.py "v=spf1..." {ip} {sender} {helo}\n    % python spf.py "v=spf1 +mx +ip4:10.0.0.1 -all" 10.0.0.1 tway@foo.com a    \n\nTo fetch an SPF record:\n    % python spf.py {domain}\n    % python spf.py wayforward.net\n\nTo test this script (and to output this usage message):\n    % python spf.py\n'
  33. import re
  34. import socket
  35. import struct
  36. import time
  37. import DNS
  38. MASK = 0xFFFFFFFFL
  39. RE_MODIFIER = re.compile('^([a-zA-Z]+)=')
  40. RE_CHAR = re.compile('%(%|_|-|(\\{[a-zA-Z][0-9]*r?[^\\}]*\\}))')
  41. RE_ARGS = re.compile('([0-9]*)(r?)([^0-9a-zA-Z]*)')
  42. JOINERS = {
  43.     'l': '.',
  44.     's': '.' }
  45. RESULTS = {
  46.     '+': 'pass',
  47.     '-': 'deny',
  48.     '?': 'unknown',
  49.     'pass': 'pass',
  50.     'deny': 'deny',
  51.     'unknown': 'unknown' }
  52. EXPLANATIONS = {
  53.     'pass': 'sender SPF verified',
  54.     'deny': 'access denied',
  55.     'unknown': 'SPF unknown' }
  56.  
  57. try:
  58.     (bool, True, False) = (bool, True, False)
  59. except NameError:
  60.     (False, True) = (0, 1)
  61.     
  62.     def bool(x):
  63.         return not (not x)
  64.  
  65.  
  66.  
  67. def check(i, s, h):
  68.     """Test an incoming MAIL FROM:<s>, from a client with ip address i.
  69. \th is the HELO/EHLO domain name.
  70.  
  71. \tReturns (result, mta-status-code, explanation) where result in
  72. \t['pass', 'unknown', 'deny', 'error'].
  73.  
  74. \tExample:
  75. \t>>> check(i='127.0.0.1', s='terry@wayforward.net', h='localhost')
  76. \t('pass', 250, 'local connections always pass')
  77. \t"""
  78.     if i.startswith('127.'):
  79.         return ('pass', 250, 'local connections always pass')
  80.     
  81.     
  82.     try:
  83.         q = query(i = i, s = s, h = h)
  84.         return q.check(q.dns_spf(q.d))
  85.     except DNS.DNSError:
  86.         return ('error', 450, 'SPF DNS Error')
  87.  
  88.  
  89.  
  90. class query(object):
  91.     """A query object keeps the relevant information about a single SPF
  92. \tquery:
  93.  
  94. \ti: ip address of SMTP client
  95. \ts: sender declared in MAIL FROM:<>
  96. \tl: local part of sender s
  97. \td: current domain, initially domain part of sender s
  98. \th: EHLO/HELO domain
  99. \tv: 'in-addr' for IPv4 clients and 'ip6' for IPv6 clients
  100. \tt: current timestamp
  101. \tp: SMTP client domain name
  102. \to: domain part of sender s
  103.  
  104. \tThis is also, by design, the same variables used in SPF macro
  105. \texpansion.
  106.  
  107. \tAlso keeps cache: DNS cache.
  108. \t"""
  109.     
  110.     def __init__(self, i, s, h):
  111.         self.i = i
  112.         self.s = s
  113.         self.h = h
  114.         (self.l, self.o) = split_email(s, h)
  115.         self.t = str(int(time.time()))
  116.         self.v = 'in-addr'
  117.         self.d = self.o
  118.         self.p = None
  119.         self.cache = { }
  120.  
  121.     
  122.     def getp(self):
  123.         if not self.p:
  124.             p = self.dns_ptr(self.i)
  125.             if len(p) > 0:
  126.                 self.p = p[0]
  127.             else:
  128.                 self.p = self.i
  129.         
  130.         return self.p
  131.  
  132.     
  133.     def check(self, spf):
  134.         """
  135. \t\tReturns (result, mta-status-code, explanation) where
  136. \t\tresult in ['deny', 'unknown', 'pass']
  137. \t\t"""
  138.         return self.check1(spf, self.d, 0)
  139.  
  140.     
  141.     def check1(self, spf, domain, recursion):
  142.         if recursion > 10:
  143.             return ('unknown', 250, 'SPF recursion limit exceeded')
  144.         
  145.         
  146.         try:
  147.             tmp = self.d
  148.             self.d = domain
  149.             return self.check0(spf, recursion)
  150.         finally:
  151.             self.d = tmp
  152.  
  153.  
  154.     
  155.     def check0(self, spf, recursion):
  156.         """Test this query information against SPF text.
  157.  
  158. \t\tReturns (result, mta-status-code, explanation) where
  159. \t\tresult in ['deny', 'unknown', 'pass']
  160. \t\t"""
  161.         if not spf:
  162.             return ('unknown', 250, 'no SPF record')
  163.         
  164.         spf = spf.split()[1:]
  165.         exps = dict(EXPLANATIONS)
  166.         redirect = None
  167.         default = 'unknown'
  168.         for m in spf:
  169.             m = RE_MODIFIER.split(m)[1:]
  170.             if len(m) != 2:
  171.                 continue
  172.             
  173.             if m[0] == 'exp':
  174.                 exps['deny'] = exps['unknown'] = self.get_explanation(m[1])
  175.                 continue
  176.             if m[0] == 'redirect':
  177.                 redirect = self.expand(m[1])
  178.                 continue
  179.             if m[0] == 'default':
  180.                 default = RESULTS.get(m[1], default)
  181.                 continue
  182.         
  183.         for m in spf:
  184.             if RE_MODIFIER.match(m):
  185.                 continue
  186.             
  187.             (m, arg, cidrlength) = parse_mechanism(m, self.d)
  188.             result = RESULTS.get(m[0])
  189.             if result:
  190.                 m = m[1:]
  191.             else:
  192.                 result = 'pass'
  193.             if m in [
  194.                 'a',
  195.                 'mx',
  196.                 'ptr',
  197.                 'exists',
  198.                 'include']:
  199.                 arg = self.expand(arg)
  200.             
  201.             if m == 'include':
  202.                 if arg != self.d:
  203.                     tmp = self.check1(self.dns_spf(arg), arg, recursion + 1)
  204.                     if tmp[0] == 'pass':
  205.                         break
  206.                     
  207.                     if tmp[0] != 'fail':
  208.                         return tmp
  209.                     
  210.                 
  211.             arg != self.d
  212.             if m == 'all':
  213.                 break
  214.                 continue
  215.             if m == 'exists':
  216.                 if len(self.dns_a(arg)) > 0:
  217.                     break
  218.                 
  219.             len(self.dns_a(arg)) > 0
  220.             if m == 'a':
  221.                 if cidrmatch(self.i, self.dns_a(arg), cidrlength):
  222.                     break
  223.                 
  224.             cidrmatch(self.i, self.dns_a(arg), cidrlength)
  225.             if m == 'mx':
  226.                 if cidrmatch(self.i, self.dns_mx(arg), cidrlength):
  227.                     break
  228.                 
  229.             cidrmatch(self.i, self.dns_mx(arg), cidrlength)
  230.             if m == 'ip4' and arg != self.d:
  231.                 if cidrmatch(self.i, [
  232.                     arg], cidrlength):
  233.                     break
  234.                 
  235.             cidrmatch(self.i, [
  236.                 arg], cidrlength)
  237.             if m == 'ptr':
  238.                 if domainmatch(self.validated_ptrs(self.i), arg):
  239.                     break
  240.                 
  241.             domainmatch(self.validated_ptrs(self.i), arg)
  242.             result = 'unknown'
  243.         elif redirect:
  244.             return self.check1(self.dns_spf(redirect), redirect, recursion + 1)
  245.         else:
  246.             result = default
  247.         if result == 'deny':
  248.             return (result, 550, exps[result])
  249.         else:
  250.             return (result, 250, exps[result])
  251.  
  252.     
  253.     def get_explanation(self, spec):
  254.         '''Expand an explanation.'''
  255.         return self.expand(''.join(self.dns_txt(self.expand(spec))))
  256.  
  257.     
  258.     def expand(self, str):
  259.         """Do SPF RFC macro expansion.
  260.  
  261. \t\tExamples:
  262. \t\t>>> q = query(s='strong-bad@email.example.com',
  263. \t\t...           h='mx.example.org', i='192.0.2.3')
  264. \t\t>>> q.p = 'mx.example.org'
  265.  
  266. \t\t>>> q.expand('%{d}')
  267. \t\t'email.example.com'
  268.  
  269. \t\t>>> q.expand('%{d4}')
  270. \t\t'email.example.com'
  271.  
  272. \t\t>>> q.expand('%{d3}')
  273. \t\t'email.example.com'
  274.  
  275. \t\t>>> q.expand('%{d2}')
  276. \t\t'example.com'
  277.  
  278. \t\t>>> q.expand('%{d1}')
  279. \t\t'com'
  280.  
  281. \t\t>>> q.expand('%{p}')
  282. \t\t'mx.example.org'
  283.  
  284. \t\t>>> q.expand('%{p2}')
  285. \t\t'example.org'
  286.  
  287. \t\t>>> q.expand('%{dr}')
  288. \t\t'com.example.email'
  289. \t
  290. \t\t>>> q.expand('%{d2r}')
  291. \t\t'example.email'
  292.  
  293. \t\t>>> q.expand('%{l}')
  294. \t\t'strong-bad'
  295.  
  296. \t\t>>> q.expand('%{l-}')
  297. \t\t'strong.bad'
  298.  
  299. \t\t>>> q.expand('%{lr}')
  300. \t\t'strong-bad'
  301.  
  302. \t\t>>> q.expand('%{lr-}')
  303. \t\t'bad.strong'
  304.  
  305. \t\t>>> q.expand('%{l1r-}')
  306. \t\t'strong'
  307.  
  308. \t\t>>> q.expand('%{ir}.%{v}._spf.%{d2}')
  309. \t\t'3.2.0.192.in-addr._spf.example.com'
  310.  
  311. \t\t>>> q.expand('%{lr-}.lp._spf.%{d2}')
  312. \t\t'bad.strong.lp._spf.example.com'
  313.  
  314. \t\t>>> q.expand('%{lr-}.lp.%{ir}.%{v}._spf.%{d2}')
  315. \t\t'bad.strong.lp.3.2.0.192.in-addr._spf.example.com'
  316.  
  317. \t\t>>> q.expand('%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}')
  318. \t\t'3.2.0.192.in-addr.strong.lp._spf.example.com'
  319.  
  320. \t\t>>> q.expand('%{p2}.trusted-domains.example.net')
  321. \t\t'example.org.trusted-domains.example.net'
  322.  
  323. \t\t>>> q.expand('%{p2}.trusted-domains.example.net')
  324. \t\t'example.org.trusted-domains.example.net'
  325.  
  326. \t\t"""
  327.         end = 0
  328.         result = ''
  329.         for i in RE_CHAR.finditer(str):
  330.             result += str[end:i.start()]
  331.             macro = str[i.start():i.end()]
  332.             if macro == '%%':
  333.                 result += '%'
  334.             elif macro == '%_':
  335.                 result += ' '
  336.             elif macro == '%-':
  337.                 result += '%20'
  338.             else:
  339.                 letter = macro[2].lower()
  340.                 if letter == 'p':
  341.                     self.getp()
  342.                 
  343.                 expansion = getattr(self, letter, '')
  344.                 if expansion:
  345.                     result += expand_one(expansion, macro[3:-1], JOINERS.get(letter))
  346.                 
  347.             end = i.end()
  348.         
  349.         return result + str[end:]
  350.  
  351.     
  352.     def dns_spf(self, domain):
  353.         '''Get the SPF record recorded in DNS for a specific domain
  354. \t\tname.  Returns None if not found, or if more than one record
  355. \t\tis found.
  356. \t\t'''
  357.         a = _[1]
  358.  
  359.     
  360.     def dns_txt(self, domainname):
  361.         return [ t for a in self.dns(domainname, 'TXT') for t in a ]
  362.  
  363.     
  364.     def dns_mx(self, domainname):
  365.         '''Get a list of IP addresses for all MX exchanges for a
  366. \t\tdomain name.
  367. \t\t'''
  368.         return [ a for mx in self.dns(domainname, 'MX') for a in self.dns_a(mx[1]) ]
  369.  
  370.     
  371.     def dns_a(self, domainname):
  372.         '''Get a list of IP addresses for a domainname.'''
  373.         return self.dns(domainname, 'A')
  374.  
  375.     
  376.     def dns_aaaa(self, domainname):
  377.         '''Get a list of IPv6 addresses for a domainname.'''
  378.         return self.dns(domainname, 'AAAA')
  379.  
  380.     
  381.     def validated_ptrs(self, i):
  382.         '''Figure out the validated PTR domain names for a given IP
  383. \t\taddress.
  384. \t\t'''
  385.         return _[1]
  386.  
  387.     
  388.     def dns_ptr(self, i):
  389.         '''Get a list of domain names for an IP address.'''
  390.         return self.dns(reverse_dots(i) + '.in-addr.arpa', 'PTR')
  391.  
  392.     
  393.     def dns(self, name, qtype):
  394.         """DNS query.
  395.  
  396. \t\tIf the result is in cache, return that.  Otherwise pull the
  397. \t\tresult from DNS, and cache ALL answers, so additional info
  398. \t\tis available for further queries later.
  399.  
  400. \t\tCNAMEs are followed.
  401.  
  402. \t\tIf there is no data, [] is returned.
  403.  
  404. \t\tpre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
  405. \t\tpost: isinstance(__return__, types.ListType)
  406. \t\t"""
  407.         result = self.cache.get((name, qtype))
  408.         cname = None
  409.         if not result:
  410.             req = DNS.DnsRequest(name, qtype = qtype)
  411.             resp = req.req()
  412.             for a in resp.answers:
  413.                 k = (a['name'], a['typename'])
  414.                 v = a['data']
  415.                 if k == (name, 'CNAME'):
  416.                     cname = v
  417.                 
  418.                 self.cache.setdefault(k, []).append(v)
  419.             
  420.             result = self.cache.get((name, qtype), [])
  421.         
  422.         if not result and cname:
  423.             result = self.dns(cname, qtype)
  424.         
  425.         return result
  426.  
  427.  
  428.  
  429. def split_email(s, h):
  430.     """Given a sender email s and a HELO domain h, create a valid tuple
  431. \t(l, d) local-part and domain-part.
  432.  
  433. \tExamples:
  434. \t>>> split_email('', 'wayforward.net')
  435. \t('postmaster', 'wayforward.net')
  436.  
  437. \t>>> split_email('foo.com', 'wayforward.net')
  438. \t('postmaster', 'foo.com')
  439.  
  440. \t>>> split_email('terry@wayforward.net', 'optsw.com')
  441. \t('terry', 'wayforward.net')
  442. \t"""
  443.     if not s:
  444.         return ('postmaster', h)
  445.     else:
  446.         parts = s.split('@', 1)
  447.         if len(parts) == 2:
  448.             return tuple(parts)
  449.         else:
  450.             return ('postmaster', s)
  451.  
  452.  
  453. def parse_mechanism(str, d):
  454.     """Breaks A, MX, IP4, and PTR mechanisms into a (name, domain,
  455. \tcidr) tuple.  The domain portion defaults to d if not present,
  456. \tthe cidr defaults to 32 if not present.
  457.  
  458. \tExamples:
  459. \t>>> parse_mechanism('a', 'foo.com')
  460. \t('a', 'foo.com', 32)
  461.  
  462. \t>>> parse_mechanism('a:bar.com', 'foo.com')
  463. \t('a', 'bar.com', 32)
  464.  
  465. \t>>> parse_mechanism('a/24', 'foo.com')
  466. \t('a', 'foo.com', 24)
  467.  
  468. \t>>> parse_mechanism('a:bar.com/16', 'foo.com')
  469. \t('a', 'bar.com', 16)
  470. \t"""
  471.     a = str.split('/')
  472.     if len(a) == 2:
  473.         a = a[0]
  474.         port = int(a[1])
  475.     else:
  476.         a = str
  477.         port = 32
  478.     b = a.split(':')
  479.     if len(b) == 2:
  480.         return (b[0], b[1], port)
  481.     else:
  482.         return (a, d, port)
  483.  
  484.  
  485. def reverse_dots(name):
  486.     """Reverse dotted IP addresses or domain names.
  487.  
  488. \tExample:
  489. \t>>> reverse_dots('192.168.0.145')
  490. \t'145.0.168.192'
  491.  
  492. \t>>> reverse_dots('email.example.com')
  493. \t'com.example.email'
  494. \t"""
  495.     a = name.split('.')
  496.     a.reverse()
  497.     return '.'.join(a)
  498.  
  499.  
  500. def domainmatch(ptrs, domainsuffix):
  501.     """grep for a given domain suffix against a list of validated PTR
  502. \tdomain names.
  503.  
  504. \tExamples:
  505. \t>>> domainmatch(['FOO.COM'], 'foo.com')
  506. \t1
  507.  
  508. \t>>> domainmatch(['moo.foo.com'], 'FOO.COM')
  509. \t1
  510.  
  511. \t>>> domainmatch(['moo.bar.com'], 'foo.com')
  512. \t0
  513.  
  514. \t"""
  515.     domainsuffix = domainsuffix.lower()
  516.     for ptr in ptrs:
  517.         ptr = ptr.lower()
  518.         if ptr == domainsuffix or ptr.endswith('.' + domainsuffix):
  519.             return True
  520.             continue
  521.     
  522.     return False
  523.  
  524.  
  525. def cidrmatch(i, ipaddrs, cidr_length = 32):
  526.     """Match an IP address against a list of other IP addresses.
  527.  
  528. \tExamples:
  529. \t>>> cidrmatch('192.168.0.45', ['192.168.0.44', '192.168.0.45'])
  530. \t1
  531.  
  532. \t>>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45'])
  533. \t0
  534.  
  535. \t>>> cidrmatch('192.168.0.43', ['192.168.0.44', '192.168.0.45'], 24)
  536. \t1
  537. \t"""
  538.     c = cidr(i, cidr_length)
  539.     for ip in ipaddrs:
  540.         if cidr(ip, cidr_length) == c:
  541.             return True
  542.             continue
  543.     
  544.     return False
  545.  
  546.  
  547. def cidr(i, n):
  548.     """Convert an IP address string with a CIDR mask into a 32-bit
  549. \tinteger.
  550.  
  551. \ti must be a string of numbers 0..255 separated by dots '.'::
  552. \tpre: forall([0 <= int(p) < 256 for p in i.split('.')])
  553.  
  554. \tn is a number of bits to mask::
  555. \tpre: 0 <= n <= 32
  556.  
  557. \tExamples:
  558. \t>>> bin2addr(cidr('192.168.5.45', 32))
  559. \t'192.168.5.45'
  560. \t>>> bin2addr(cidr('192.168.5.45', 24))
  561. \t'192.168.5.0'
  562. \t>>> bin2addr(cidr('192.168.0.45', 8))
  563. \t'192.0.0.0'
  564. \t"""
  565.     return ~(MASK >> n) & MASK & addr2bin(i)
  566.  
  567.  
  568. def addr2bin(str):
  569.     """Convert a string IPv4 address into an unsigned integer.
  570.  
  571. \tExamples::
  572. \t>>> addr2bin('127.0.0.1')
  573. \t2130706433L
  574.  
  575. \t>>> addr2bin('127.0.0.1') == socket.INADDR_LOOPBACK
  576. \t1
  577.  
  578. \t>>> addr2bin('255.255.255.254')
  579. \t4294967294L
  580.  
  581. \t>>> addr2bin('192.168.0.1')
  582. \t3232235521L
  583.  
  584. \tUnlike DNS.addr2bin, the n, n.n, and n.n.n forms for IP addresses
  585. \tare handled as well::
  586. \t>>> addr2bin('10.65536')
  587. \t167837696L
  588. \t>>> 10 * (2 ** 24) + 65536
  589. \t167837696
  590.  
  591. \t>>> addr2bin('10.93.512')
  592. \t173867520L
  593. \t>>> 10 * (2 ** 24) + 93 * (2 ** 16) + 512
  594. \t173867520
  595. \t"""
  596.     return struct.unpack('!L', socket.inet_aton(str))[0]
  597.  
  598.  
  599. def bin2addr(addr):
  600.     """Convert a numeric IPv4 address into string n.n.n.n form.
  601.  
  602. \tExamples::
  603. \t>>> bin2addr(socket.INADDR_LOOPBACK)
  604. \t'127.0.0.1'
  605.  
  606. \t>>> bin2addr(socket.INADDR_ANY)
  607. \t'0.0.0.0'
  608.  
  609. \t>>> bin2addr(socket.INADDR_NONE)
  610. \t'255.255.255.255'
  611. \t"""
  612.     return socket.inet_ntoa(struct.pack('!L', addr))
  613.  
  614.  
  615. def expand_one(expansion, str, joiner):
  616.     if not str:
  617.         return expansion
  618.     
  619.     (len, reverse, delimiters) = RE_ARGS.split(str)[1:4]
  620.     if not delimiters:
  621.         delimiters = '.'
  622.     
  623.     expansion = split(expansion, delimiters, joiner)
  624.     if reverse:
  625.         expansion.reverse()
  626.     
  627.     if len:
  628.         expansion = expansion[-int(len) * 2 + 1:]
  629.     
  630.     return ''.join(expansion)
  631.  
  632.  
  633. def split(str, delimiters, joiner = None):
  634.     """Split a string into pieces by a set of delimiter characters.  The
  635. \tresulting list is delimited by joiner, or the original delimiter if
  636. \tjoiner is not specified.
  637.  
  638. \tExamples:
  639. \t>>> split('192.168.0.45', '.')
  640. \t['192', '.', '168', '.', '0', '.', '45']
  641.  
  642. \t>>> split('terry@wayforward.net', '@.')
  643. \t['terry', '@', 'wayforward', '.', 'net']
  644.  
  645. \t>>> split('terry@wayforward.net', '@.', '.')
  646. \t['terry', '.', 'wayforward', '.', 'net']
  647. \t"""
  648.     result = []
  649.     element = ''
  650.     for c in str:
  651.         if c in delimiters:
  652.             result.append(element)
  653.             element = ''
  654.             if joiner:
  655.                 result.append(joiner)
  656.             else:
  657.                 result.append(c)
  658.         joiner
  659.         element += c
  660.     
  661.     result.append(element)
  662.     return result
  663.  
  664.  
  665. def _test():
  666.     import doctest
  667.     import spf
  668.     return doctest.testmod(spf)
  669.  
  670. DNS.DiscoverNameServers()
  671. if __name__ == '__main__':
  672.     import sys
  673.     if len(sys.argv) == 1:
  674.         print USAGE
  675.         _test()
  676.     elif len(sys.argv) == 2:
  677.         q = query(i = '127.0.0.1', s = 'localhost', h = 'unknown')
  678.         print q.dns_spf(sys.argv[1])
  679.     elif len(sys.argv) == 4:
  680.         print check(i = sys.argv[1], s = sys.argv[2], h = sys.argv[3])
  681.     elif len(sys.argv) == 5:
  682.         (i, s, h) = sys.argv[2:]
  683.         q = query(i = i, s = s, h = h)
  684.         print q.check(sys.argv[1])
  685.     else:
  686.         print USAGE
  687.  
  688.